/* 
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.harmony.jndi.internal;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;

import org.apache.harmony.javax.naming.ConfigurationException;
import org.apache.harmony.javax.naming.Context;
import org.apache.harmony.javax.naming.NamingException;
import org.apache.harmony.jndi.internal.nls.Messages;

/**
 * This is a utility class that reads environment properties.
 */
public final class EnvironmentReader {

	/*
	 * Wrapper interface for JNDI properties source.
	 */
	private interface JNDIPropertiesSource {
		// Get a JNDI property with the specified name
		String getProperty(final String propName);
	}

	/*
	 * Wrapper class for system properties source.
	 */
	private static class SystemPropertiesSource implements JNDIPropertiesSource {

		public SystemPropertiesSource() {
			super();
		}

		@Override
		public String getProperty(final String propName) {
			return System.getProperty(propName);
		}
	}

	// The name of application resource files
	private static final String APPLICATION_RESOURCE_FILE = "jndi.properties"; //$NON-NLS-1$

	// The name of provider resource file
	private static final String PROVIDER_RESOURCE_FILE = "jndiprovider.properties"; //$NON-NLS-1$

	/*
	 * Get the required 7 JNDI properties from JNDI properties source. This
	 * method is designed as package visibility to improve performance when
	 * called by anonymous inner classes.
	 * 
	 * @return a hashtable holding the required properties.
	 */
	static Hashtable<Object, Object> filterProperties(
			final JNDIPropertiesSource source) {
		final Hashtable<Object, Object> filteredProperties = new Hashtable<Object, Object>();
		String propValue = null;

		propValue = source.getProperty(Context.INITIAL_CONTEXT_FACTORY);
		if (null != propValue) {
			filteredProperties.put(Context.INITIAL_CONTEXT_FACTORY, propValue);
		}

		propValue = source.getProperty(Context.DNS_URL);
		if (null != propValue) {
			filteredProperties.put(Context.DNS_URL, propValue);
		}

		propValue = source.getProperty(Context.PROVIDER_URL);
		if (null != propValue) {
			filteredProperties.put(Context.PROVIDER_URL, propValue);
		}

		propValue = source.getProperty(Context.OBJECT_FACTORIES);
		if (null != propValue) {
			filteredProperties.put(Context.OBJECT_FACTORIES, propValue);
		}

		propValue = source.getProperty(Context.STATE_FACTORIES);
		if (null != propValue) {
			filteredProperties.put(Context.STATE_FACTORIES, propValue);
		}

		propValue = source.getProperty(Context.URL_PKG_PREFIXES);
		if (null != propValue) {
			filteredProperties.put(Context.URL_PKG_PREFIXES, propValue);
		}

		return filteredProperties;
	}

	/*
	 * Get the list of the specified factory names from the supplied environment
	 * and the resource provider files of the given Context.
	 * 
	 * @param envmt The supplied environment. @param ctx The Context whose
	 * resource provider files will be read. @param key The name of the factory.
	 * 
	 * @return The list of the desired factory names. @throws NamingException If
	 * an error occurs when reading the provider resource files.
	 */
	public static String[] getFactoryNamesFromEnvironmentAndProviderResource(
			Hashtable<?, ?> envmt, Context ctx, String key)
			throws NamingException {

		final List<String> fnames = new ArrayList<String>();

		// collect tokens from envmt with key
		if (null != envmt) {
			final String str = (String) envmt.get(key);
			if (null != str) {
				final StringTokenizer st = new StringTokenizer(str, ":"); //$NON-NLS-1$
				while (st.hasMoreTokens()) {
					fnames.add(st.nextToken());
				}
			}
		}
		// collect tokens from ctx's provider resource file
		if (null != ctx) {
			final Hashtable<Object, Object> h = new Hashtable<Object, Object>();
			// read provider resource file from ctx's package
			EnvironmentReader.readProviderResourceFiles(ctx, h);
			final String str = (String) h.get(key);
			if (null != str) {
				final StringTokenizer st = new StringTokenizer(str, ":"); //$NON-NLS-1$
				while (st.hasMoreTokens()) {
					fnames.add(st.nextToken());
				}
			}
		}
		// if key is Context.URL_PKG_PREFIXES, append "com.sun.jndi.url" and
		// "org.apache.harmony.jndi.provider" at the end
		if (Context.URL_PKG_PREFIXES.equals(key)) {
			fnames.add("com.sun.jndi.url"); //$NON-NLS-1$
			fnames.add("org.apache.harmony.jndi.provider"); //$NON-NLS-1$
		}
		// return factory names
		return fnames.toArray(new String[fnames.size()]);
	}

	/*
	 * Merge additional properties with already read ones.
	 * 
	 * @param src - the source containing additional properties @param dst - the
	 * destination to put additional properties @param valueAddToList - whether
	 * to add new values of C-type properties
	 */
	public static void mergeEnvironment(final Hashtable<?, ?> src,
			final Hashtable<Object, Object> dst, final boolean valueAddToList) {

		Object key = null;
		String val = null;
		final Enumeration<?> keys = src.keys();

		while (keys.hasMoreElements()) {
			key = keys.nextElement();

			if (!dst.containsKey(key)) {
				/*
				 * If this property doesn't exist yet, add it.
				 */
				dst.put(key, src.get(key));
			} else if (valueAddToList
					&& (Context.OBJECT_FACTORIES.equals(key)
							|| Context.STATE_FACTORIES.equals(key) || Context.URL_PKG_PREFIXES
								.equals(key))) {
				/*
				 * Otherwise, if this property can contain a list of values, add
				 * the additional values if the flag "valueAddToList" is true.
				 */

				// Read the original value
				val = (String) dst.get(key);
				// Values are combined into a single list separated by colons.
				val = val + ":" + src.get(key); //$NON-NLS-1$
				// The final value becomes the resulting value of that property
				dst.put(key, val);
			} else {
				/*
				 * Otherwise, ignore the found value.
				 */
			}
		}
	}

	/*
	 * Read application/applet resource files.
	 * 
	 * @param existingProps - existing properties, cannot be null.
	 */
	public static Hashtable<Object, Object> readApplicationResourceFiles(
			final Hashtable<Object, Object> existingProps)
			throws NamingException {
		// Use privileged code to read the application resource files
		try {
			AccessController
					.doPrivileged(new PrivilegedExceptionAction<Void>() {
						@Override
						public Void run() throws NamingException {
							readMultipleResourceFiles(
									APPLICATION_RESOURCE_FILE, existingProps,
									Thread.currentThread()
											.getContextClassLoader());
							return null;
						}
					});
		} catch (final PrivilegedActionException e) {
			final Exception rootCause = e.getException();
			if (rootCause instanceof NamingException) {
				throw (NamingException) rootCause;
			} else if (rootCause instanceof RuntimeException) {
				throw (RuntimeException) rootCause;
			} else {
				// This should not happen.
			}
		}
		return existingProps;
	}

	/*
	 * Read the properties file "java.home"/lib/jndi.properties. Pay attention
	 * to the privileged code for accessing this external resource file. This is
	 * required if JNDI is run in Applet or other applications which only have
	 * limited permissions to access certain resources.
	 * 
	 * @param existingProps - existing properties, cannot be null.
	 */
	public static Hashtable<Object, Object> readLibraryResourceFile(
			final Hashtable<Object, Object> existingProps)
			throws NamingException {
		final String sep = System.getProperty("file.separator"); //$NON-NLS-1$

		String resPath = null;
		// Construct the full filename of "java.home"/lib/jndi.properties
		resPath = AccessController.doPrivileged(new PrivilegedAction<String>() {
			@Override
			public String run() {
				return System.getProperty("java.home"); //$NON-NLS-1$ 
			}
		});

		if (!resPath.endsWith(sep)) {
			resPath += sep;
		}
		resPath += "lib" + sep + APPLICATION_RESOURCE_FILE; //$NON-NLS-1$

		// Try to read this properties if it exists
		InputStream is = null;
		final File resFile = new File(resPath);
		final Properties p = new Properties();
		// Use privileged code to determine whether the file exists
		final boolean resFileExists = AccessController.doPrivileged(
				new PrivilegedAction<Boolean>() {
					@Override
					public Boolean run() {
						return Boolean.valueOf(resFile.exists());
					}
				}).booleanValue();
		if (resFileExists) {
			try {
				// Use privileged code to read the file
				is = AccessController
						.doPrivileged(new PrivilegedExceptionAction<FileInputStream>() {
							@Override
							public FileInputStream run() throws IOException {
								final FileInputStream localInputStream = new FileInputStream(
										resFile);
								p.load(localInputStream);
								return localInputStream;
							}
						});
				mergeEnvironment(p, existingProps, true);
			} catch (final PrivilegedActionException e) {
				// Can't read "java.home"/lib/jndi.properties
				// jndi.25=Failed to read JNDI resource files in java home
				// library.
				final ConfigurationException newEx = new ConfigurationException(
						Messages.getString("jndi.25")); //$NON-NLS-1$
				newEx.setRootCause(e.getException());
				throw newEx;
			} finally {
				try {
					if (null != is) {
						is.close();
					}
				} catch (final IOException ex) {
					// Ignore closing exception
				}
			}
		}
		return existingProps;
	}

	/*
	 * Read multiple resource files from the classpaths given the file name.
	 * This method is designed as package visibility to improve performance when
	 * called by anonymous inner classes.
	 * 
	 * @param name - the name of the resource file @param existingProps -
	 * existing properties, cannot be null @param filter - to filter properties
	 */
	static Hashtable<Object, Object> readMultipleResourceFiles(
			final String name, final Hashtable<Object, Object> existingProps,
			ClassLoader cl) throws NamingException {

		if (null == cl) {
			cl = ClassLoader.getSystemClassLoader();
		}

		Enumeration<URL> e = null;
		try {
			// Load all resource files
			e = cl.getResources(name);
		} catch (final IOException ex) {
			// Unexpected ClassLoader exception
			// jndi.23=Failed to load JNDI resource files.
			final ConfigurationException newEx = new ConfigurationException(
					Messages.getString("jndi.23")); //$NON-NLS-1$
			newEx.setRootCause(ex);
			throw newEx;
		}

		// Read all the loaded properties and merge
		URL url = null;
		InputStream is = null;
		final Properties p = new Properties();
		while (e.hasMoreElements()) {
			url = e.nextElement();
			try {
				if (null != (is = url.openStream())) {
					p.load(is);
					mergeEnvironment(p, existingProps, true);
					p.clear();
				}
			} catch (final IOException ex) {
				// Can't read this resource file
				// jndi.24=Failed to read JNDI resource files.
				final ConfigurationException newEx = new ConfigurationException(
						Messages.getString("jndi.24")); //$NON-NLS-1$
				newEx.setRootCause(ex);
				throw newEx;
			} finally {
				try {
					if (null != is) {
						is.close();
					}
				} catch (final IOException ex) {
					// Ignore closing exception
				} finally {
					is = null;
				}
			}
		}
		return existingProps;
	}

	/*
	 * Read the service provider resource file.
	 * 
	 * @param context - the context @param existingProps - existing properties,
	 * cannot be null.
	 */
	public static Hashtable<Object, Object> readProviderResourceFiles(
			final Context context, final Hashtable<Object, Object> existingProps)
			throws NamingException {

		final String factory = context.getClass().getName();
		String resPath = null;
		final int len = factory.lastIndexOf('.');

		// Construct the full filename of the service provider resource file
		if (-1 == len) {
			// Default package
			resPath = PROVIDER_RESOURCE_FILE;
		} else {
			// Replace "." with '/'
			resPath = factory.substring(0, len + 1);
			resPath = resPath.replace('.', '/');
			resPath += PROVIDER_RESOURCE_FILE;
		}

		// Use privileged code to read the provider resource files
		try {
			final String finalResPath = resPath;
			AccessController
					.doPrivileged(new PrivilegedExceptionAction<String>() {
						@Override
						public String run() throws NamingException {
							readMultipleResourceFiles(finalResPath,
									existingProps, context.getClass()
											.getClassLoader());
							return null;
						}
					});
		} catch (final PrivilegedActionException e) {
			final Exception rootCause = e.getException();
			if (rootCause instanceof NamingException) {
				throw (NamingException) rootCause;
			} else if (rootCause instanceof RuntimeException) {
				throw (RuntimeException) rootCause;
			} else {
				// This should not happen.
				throw new AssertionError(rootCause);
			}
		}
		return existingProps;
	}

	/*
	 * Read the required 7 JNDI properties from system properties and merge with
	 * existing properties. Note that the values of C-type properties are only
	 * included when no corresponding value is presented in existing properties.
	 * 
	 * @param existingProps - existing properties
	 */
	public static void readSystemProperties(
			final Hashtable<Object, Object> existingProps) {
		/*
		 * Privileged code is used to access system properties. This is required
		 * if JNDI is run in Applet or other applications which only have
		 * limited permissions to access certain resources.
		 */
		final Hashtable<Object, Object> systemProperties = AccessController
				.doPrivileged(new PrivilegedAction<Hashtable<Object, Object>>() {
					@Override
					public Hashtable<Object, Object> run() {
						return filterProperties(new SystemPropertiesSource());
					}
				});
		mergeEnvironment(systemProperties, existingProps, false);
	}

	// Not allowed to create an instance
	private EnvironmentReader() {
		super();
	}

}
